spb/worthdoing Public
Autonomous investigation agent that discovers, challenges, and ranks things genuinely worth doing — Claude + Firecrawl, Next.js 16, PostgreSQL
TypeScript 91.5%
SQL 5.8%
CSS 2.2%
1/**2 * WorthDoing.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: src/app/api/opportunities/[id]/route.ts6 * Description: Full opportunity report data — report, scores, evidence with sources, competitors, skeptic case.7 */8import { NextRequest, NextResponse } from "next/server";9import { and, asc, eq, inArray } from "drizzle-orm";10import { z } from "zod";11import { db } from "@/lib/db/client";12import {13 opportunities,14 opportunityScores,15 opportunityEvidence,16 opportunityCompetitors,17 competitors,18 evidence,19 sources,20 investigations,21 hypotheses,22} from "@/lib/db/schema";2324export const dynamic = "force-dynamic";2526export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {27 const { id } = await ctx.params;28 if (!z.string().uuid().safeParse(id).success) {29 return NextResponse.json({ error: "Invalid opportunity id." }, { status: 400 });30 }3132 const [opp] = await db.select().from(opportunities).where(eq(opportunities.id, id));33 if (!opp) return NextResponse.json({ error: "Opportunity not found." }, { status: 404 });3435 const [inv] = await db.select().from(investigations).where(eq(investigations.id, opp.investigationId));36 const hyp = opp.hypothesisId37 ? (await db.select().from(hypotheses).where(eq(hypotheses.id, opp.hypothesisId)))[0]38 : null;3940 const scores = await db.select().from(opportunityScores).where(eq(opportunityScores.opportunityId, id));4142 const links = await db.select().from(opportunityEvidence).where(eq(opportunityEvidence.opportunityId, id));43 const evidenceIds = links.map((l) => l.evidenceId);44 const evRows = evidenceIds.length45 ? await db46 .select({47 id: evidence.id,48 kind: evidence.kind,49 quote: evidence.quote,50 summary: evidence.summary,51 strength: evidence.strength,52 createdAt: evidence.createdAt,53 sourceUrl: sources.canonicalUrl,54 sourceTitle: sources.title,55 sourceDomain: sources.domain,56 })57 .from(evidence)58 .innerJoin(sources, eq(sources.id, evidence.sourceId))59 .where(and(eq(evidence.investigationId, opp.investigationId), inArray(evidence.id, evidenceIds)))60 .orderBy(asc(evidence.createdAt))61 : [];62 const roleByEvidence = new Map(links.map((l) => [l.evidenceId, l.role]));6364 const comps = await db65 .select({66 name: competitors.name,67 url: competitors.url,68 description: competitors.description,69 note: opportunityCompetitors.note,70 })71 .from(opportunityCompetitors)72 .innerJoin(competitors, eq(competitors.id, opportunityCompetitors.competitorId))73 .where(eq(opportunityCompetitors.opportunityId, id));7475 return NextResponse.json({76 opportunity: {77 id: opp.id,78 title: opp.title,79 summary: opp.summary,80 problem: opp.problem,81 whyNow: opp.whyNow,82 risks: opp.risks,83 skepticCase: opp.skepticCase,84 reportMd: opp.reportMd,85 status: opp.status,86 worthScore: opp.worthScore,87 evidenceConfidence: opp.evidenceConfidence,88 createdAt: opp.createdAt,89 },90 investigation: inv91 ? { id: inv.id, objective: inv.objective, status: inv.status, completedAt: inv.completedAt }92 : null,93 hypothesis: hyp94 ? { id: hyp.id, title: hyp.title, statement: hyp.statement, status: hyp.status, confidence: hyp.confidence }95 : null,96 scores,97 evidence: evRows.map((e, i) => ({ ...e, index: i + 1, role: roleByEvidence.get(e.id) ?? "context" })),98 competitors: comps,99 });100}101